using System;
using System.Globalization;
using System.Web;

namespace Subtext.Akismet
{
    /// <summary>
    /// The client class used to communicate with the 
    /// <see href="http://akismet.com/">Akismet</see> service.
    /// </summary>
    public class AkismetClient
    {
        private const string checkUrlFormat = "http://{0}.rest.akismet.com/1.1/comment-check";
        private const string submitHamUrlFormat = "http://{0}.rest.akismet.com/1.1/submit-ham";
        private const string submitSpamUrlFormat = "http://{0}.rest.akismet.com/1.1/submit-spam";
        private static readonly Uri verifyUrl = new Uri("http://rest.akismet.com/1.1/verify-key");
        private static readonly string version = typeof (HttpClient).Assembly.GetName().Version.ToString();
        private readonly HttpClient httpClient;
        private string apiKey;
        private Uri blogUrl;

        private Uri checkUrl;
        private Uri submitHamUrl;
        private Uri submitSpamUrl;
        private int timeout = 5000;
        private string userAgent = null;

        /// <summary>
        /// Initializes a new instance of the <see cref="AkismetClient"/> class.
        /// </summary>
        /// <remarks>
        /// This constructor takes in all the dependencies to allow for 
        /// dependency injection and unit testing. Seems like overkill, 
        /// but it's worth it.
        /// </remarks>
        /// <param name="apiKey">The Akismet API key.</param>
        /// <param name="blogUrl">The root url of the blog.</param>
        /// <param name="httpClient">Client class used to make the underlying requests.</param>
        public AkismetClient(string apiKey, Uri blogUrl, HttpClient httpClient)
        {
            if (apiKey == null)
            {
                throw new ArgumentNullException("apiKey", "The akismet Api Key must be specified");
            }

            if (blogUrl == null)
            {
                throw new ArgumentNullException("blogUrl", "The blog's url must be specified");
            }

            if (httpClient == null)
            {
                throw new ArgumentNullException("httpClient", "Must supply an http client");
            }

            this.apiKey = apiKey;
            this.blogUrl = blogUrl;
            this.httpClient = httpClient;

            SetServiceUrls();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="AkismetClient"/> class.
        /// </summary>
        /// <param name="apiKey">The Akismet API key.</param>
        /// <param name="blogUrl">The root url of the blog.</param>
        public AkismetClient(string apiKey, Uri blogUrl) : this(apiKey, blogUrl, new HttpClient())
        {
        }

        /// <summary>
        /// Gets or sets the Akismet API key.
        /// </summary>
        /// <value>The API key.</value>
        public string ApiKey
        {
            get { return apiKey ?? string.Empty; }
            set
            {
                apiKey = value ?? string.Empty;
                SetServiceUrls();
            }
        }

        /// <summary>
        /// Gets or sets the Usera Agent for the Akismet Client.  
        /// Do not confuse this with the user agent for the comment 
        /// being checked.
        /// </summary>
        /// <value>The API key.</value>
        public string UserAgent
        {
            get { return userAgent ?? BuildUserAgent("DasBlog", version); }
            set { userAgent = value; }
        }

        /// <summary>
        /// Gets or sets the timeout in milliseconds for the http request to Akismet. 
        /// By default 5000 (5 seconds).
        /// </summary>
        /// <value>The timeout.</value>
        public int Timeout
        {
            get { return timeout; }
            set { timeout = value; }
        }

        /// <summary>
        /// Gets or sets the root URL to the blog.
        /// </summary>
        /// <value>The blog URL.</value>
        public Uri BlogUrl
        {
            get { return blogUrl; }
            set { blogUrl = value; }
        }

        private void SetServiceUrls()
        {
            submitHamUrl = new Uri(String.Format(submitHamUrlFormat, apiKey));
            submitSpamUrl = new Uri(String.Format(submitSpamUrlFormat, apiKey));
            checkUrl = new Uri(String.Format(checkUrlFormat, apiKey));
        }

        /// <summary>
        /// Helper method for building a user agent string in the format 
        /// preferred by Akismet.
        /// </summary>
        /// <param name="applicationName">Name of the application.</param>
        /// <param name="appVersion">The version of the app.</param>
        /// <returns></returns>
        public static string BuildUserAgent(string applicationName, string appVersion)
        {
            return string.Format("{0}/{1} | Akismet/1.11", applicationName, version);
        }

        /// <summary>
        /// Verifies the API key.  You really only need to 
        /// call this once, perhaps at startup.
        /// </summary>
        /// <returns></returns>
        public bool VerifyApiKey()
        {
            string parameters = "key=" + HttpUtility.UrlEncode(ApiKey) + "&blog=" +
                                HttpUtility.UrlEncode(BlogUrl.ToString());
            string result = httpClient.PostRequest(verifyUrl, UserAgent, Timeout, parameters);

            if (String.IsNullOrEmpty(result))
                throw new InvalidResponseException("Akismet returned an empty response");

            return (System.String.Compare("valid", result, true) == 0);
        }

        /// <summary>
        /// Checks the comment and returns true if it is spam, otherwise false.
        /// </summary>
        /// <param name="comment"></param>
        /// <returns></returns>
        public bool CheckCommentForSpam(IComment comment)
        {
            string result = SubmitComment(comment, checkUrl);

            if (String.IsNullOrEmpty(result))
            {
                throw new InvalidResponseException("Akismet returned an empty response");
            }

            if (result != "true" && result != "false")
            {
                throw new InvalidResponseException(
                    string.Format("Received the response '{0}' from Akismet. Probably a bad API key.", result));
            }

            return bool.Parse(result);
        }

        /// <summary>
        /// Submits a comment to Akismet that should have been 
        /// flagged as SPAM, but was not flagged by Akismet.
        /// </summary>
        /// <param name="comment"></param>
        /// <returns></returns>
        public void SubmitSpam(IComment comment)
        {
            SubmitComment(comment, submitSpamUrl);
        }

        /// <summary>
        /// Submits a comment to Akismet that should not have been 
        /// flagged as SPAM (a false positive).
        /// </summary>
        /// <param name="comment"></param>
        /// <returns></returns>
        public void SubmitHam(IComment comment)
        {
            SubmitComment(comment, submitHamUrl);
        }

        private string SubmitComment(IComment comment, Uri url)
        {
            //Not too many concatenations.  Might not need a string builder.
            string parameters = "blog=" + HttpUtility.UrlEncode(blogUrl.ToString())
                                + "&user_ip=" + comment.IpAddress
                                + "&user_agent=" + HttpUtility.UrlEncode(comment.UserAgent);

            if (!String.IsNullOrEmpty(comment.Referer))
            {
                parameters += "&referer=" + HttpUtility.UrlEncode(comment.Referer);
            }

            if (comment.Permalink != null)
            {
                parameters += "&permalink=" + HttpUtility.UrlEncode(comment.Permalink.ToString());
            }

            if (!String.IsNullOrEmpty(comment.CommentType))
            {
                parameters += "&comment_type=" + HttpUtility.UrlEncode(comment.CommentType);
            }

            if (!String.IsNullOrEmpty(comment.Author))
            {
                parameters += "&comment_author=" + HttpUtility.UrlEncode(comment.Author);
            }

            if (!String.IsNullOrEmpty(comment.AuthorEmail))
            {
                parameters += "&comment_author_email=" + HttpUtility.UrlEncode(comment.AuthorEmail);
            }

            if (comment.AuthorUrl != null)
            {
                parameters += "&comment_author_url=" + HttpUtility.UrlEncode(comment.AuthorUrl.ToString());
            }

            if (!String.IsNullOrEmpty(comment.Content))
            {
                parameters += "&comment_content=" + HttpUtility.UrlEncode(comment.Content);
            }

            if (comment.ServerEnvironmentVariables != null)
            {
                foreach (string key in comment.ServerEnvironmentVariables)
                {
                    parameters += "&" + key + "=" + HttpUtility.UrlEncode(comment.ServerEnvironmentVariables[key]);
                }
            }

            return httpClient.PostRequest(url, UserAgent, Timeout, parameters).ToLower(CultureInfo.InvariantCulture);
        }
    }

    /// <summary>
    /// Provides .NET 2.0 System.String features
    /// </summary>
    /// <remarks>
    /// The idea is to reduce the differences between our version of the library and Subtext's version.
    /// The fewer differences in the codebase, the easier it will be to merge in new features they add (in theory)
    /// </remarks>
    internal class String
    {
        public static bool IsNullOrEmpty(string stringToCheck)
        {
            if (stringToCheck == null) return true;
            return (stringToCheck.Length == 0);
        }

        public static string Format(string format, params object[] args)
        {
            return System.String.Format(format, args);
        }
    }
}